TypeScript · 9148 bytes Raw Blame History
1 'use client';
2
3 import { useState, useEffect } from 'react';
4 import Link from 'next/link';
5 import { useParams } from 'next/navigation';
6 import { getPersonDetail, PersonDetailWithContributions } from '@/lib/api';
7 import Header from '@/components/Header';
8 import ContributionForm from '@/components/ContributionForm';
9 import ContributionDisplay from '@/components/ContributionDisplay';
10
11 export default function PersonPage() {
12 const params = useParams();
13 const personId = parseInt(params.id as string);
14
15 const [person, setPerson] = useState<PersonDetailWithContributions | null>(null);
16 const [loading, setLoading] = useState(true);
17 const [error, setError] = useState<string | null>(null);
18 const [pdfError, setPdfError] = useState(false);
19 const [pdfUrl, setPdfUrl] = useState<string | null>(null);
20 const [loadingPdf, setLoadingPdf] = useState(false);
21
22 useEffect(() => {
23 async function fetchData() {
24 try {
25 const personData = await getPersonDetail(personId);
26 setPerson(personData);
27 } catch (err) {
28 setError(err instanceof Error ? err.message : 'Failed to load person details');
29 console.error(err);
30 } finally {
31 setLoading(false);
32 }
33 }
34
35 fetchData();
36 }, [personId]);
37
38 useEffect(() => {
39 async function fetchPdfUrl() {
40 if (person && person.pdf_key && !pdfUrl) {
41 setLoadingPdf(true);
42 try {
43 const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
44 setPdfUrl(`${apiUrl}/memorial/persons/${person.id}/pdf/`);
45 } catch (err) {
46 console.error('Failed to set PDF URL:', err);
47 setPdfError(true);
48 } finally {
49 setLoadingPdf(false);
50 }
51 }
52 }
53
54 fetchPdfUrl();
55 }, [person, pdfUrl]);
56
57 const handleContributionSuccess = async () => {
58 // Reload person data to get updated contributions
59 try {
60 const personData = await getPersonDetail(personId);
61 setPerson(personData);
62 } catch (err) {
63 console.error('Failed to reload person data:', err);
64 }
65 };
66
67 if (loading) {
68 return (
69 <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
70 <p className="text-gray-600 text-xl">Loading...</p>
71 </div>
72 );
73 }
74
75 if (error || !person) {
76 return (
77 <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
78 <div className="text-center">
79 <p className="text-red-600 mb-4 text-xl">{error || 'Person not found'}</p>
80 <Link href="/" className="text-vmi-red hover:text-vmi-dark-red underline font-semibold">
81 Return to Home
82 </Link>
83 </div>
84 </div>
85 );
86 }
87
88 const displayName = person.full_display_name ?
89 person.full_display_name.replace(person.rank + ' ', '').replace(person.rank + ', ', '')
90 : person.display_name;
91
92 return (
93 <div className="min-h-screen bg-vmi-cream">
94 <Header
95 breadcrumbs={[
96 { label: 'Home', href: '/' },
97 { label: person.conflict_name, href: `/memorial/conflict/${person.conflict}` },
98 { label: person.display_name }
99 ]}
100 />
101
102 {/* Main Content */}
103 <main className="max-w-6xl mx-auto px-4 py-12">
104 {/* Person Header */}
105 <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
106 <h1 className="text-4xl font-black text-vmi-red mb-2 flex items-center gap-2">
107 {displayName}
108 {person.pdf_key && (
109 <span title="Memorial document available" className="inline-block align-middle">
110 {/* Simple document icon SVG */}
111 <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#b91c1c" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-file-text">
112 <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
113 <polyline points="14 2 14 8 20 8" />
114 <line x1="16" y1="13" x2="8" y2="13" />
115 <line x1="16" y1="17" x2="8" y2="17" />
116 <line x1="10" y1="9" x2="9" y2="9" />
117 </svg>
118 </span>
119 )}
120 </h1>
121
122 {/* Rank and Unit subtitle */}
123 {(person.rank || person.unit) && (
124 <div className="mb-6">
125 {person.rank && (
126 <p className="text-xl font-bold text-gray-700">{person.rank}</p>
127 )}
128 {person.unit && (
129 <p className="text-lg text-gray-600 italic">{person.unit}</p>
130 )}
131 </div>
132 )}
133
134 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-gray-800">
135 <div className="space-y-3">
136 {person.class_year && (
137 <p className="text-lg">
138 <span className="font-bold text-gray-700">Class Year:</span> {person.class_year}
139 </p>
140 )}
141 <p className="text-lg">
142 <span className="font-bold text-gray-700">Conflict:</span> {person.conflict_name}
143 </p>
144 </div>
145 <div className="space-y-3">
146 {person.death_date_display && (
147 <p className="text-lg">
148 <span className="font-bold text-gray-700">Date of Death:</span>{' '}
149 {person.death_date_display}
150 </p>
151 )}
152 </div>
153 </div>
154 </div>
155
156 {/* Death Description Section */}
157 {person.death_description && (
158 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 mb-12 shadow-xl">
159 <h2 className="text-2xl font-bold mb-4 text-vmi-red">
160 Circumstances of Death
161 </h2>
162 <p className="text-lg text-gray-800 leading-relaxed italic">
163 {person.death_description}
164 </p>
165 </div>
166 )}
167
168 {/* PDF Viewer */}
169 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mb-12">
170 <h2 className="text-3xl font-bold mb-6 text-center text-vmi-red">
171 Memorial Portrait
172 </h2>
173
174 {person.pdf_key ? (
175 <div className="relative">
176 {loadingPdf ? (
177 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
178 <p className="text-gray-700 text-lg">Loading PDF...</p>
179 </div>
180 ) : pdfError ? (
181 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
182 <p className="text-gray-700 mb-4 text-lg">
183 Unable to load PDF viewer.
184 </p>
185 {pdfUrl && (
186 <a
187 href={pdfUrl}
188 target="_blank"
189 rel="noopener noreferrer"
190 className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:shadow-lg hover:scale-105 transition-all font-semibold"
191 >
192 Open PDF in New Tab
193 </a>
194 )}
195 </div>
196 ) : pdfUrl ? (
197 <div className="border-2 border-gray-400 rounded-lg overflow-hidden">
198 <iframe
199 src={`${pdfUrl}#toolbar=0&navpanes=0`}
200 className="w-full h-[800px]"
201 onError={() => setPdfError(true)}
202 title={`Memorial document for ${person.display_name}`}
203 />
204 <div className="p-6 bg-gray-100 text-center border-t-2 border-gray-400">
205 <a
206 href={pdfUrl}
207 target="_blank"
208 rel="noopener noreferrer"
209 className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:shadow-lg hover:scale-105 transition-all font-semibold"
210 >
211 Open PDF in Full Screen
212 </a>
213 </div>
214 </div>
215 ) : null}
216 </div>
217 ) : (
218 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
219 <p className="text-gray-700 text-lg">
220 No memorial document available yet.
221 </p>
222 </div>
223 )}
224 </div>
225
226 {/* Community Contributions Section */}
227 <ContributionForm
228 personId={person.id}
229 personName={displayName}
230 onSuccess={handleContributionSuccess}
231 />
232
233 {/* Display Approved Contributions */}
234 {person.contributions && person.contributions.length > 0 && (
235 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mt-8">
236 <ContributionDisplay contributions={person.contributions} />
237 </div>
238 )}
239 </main>
240 </div>
241 );
242 }